第 9 章:使用 Terraform 模組
前面練習過程中,我們寫了不少的 Terraform 組態檔,但都同樣是要建立網頁伺服器的,我想應該有辦法製作可重複使用的組態,減少一些重工的行為。
接下來要認識的 Terraform 模組 (Modules) 就可以解決這些問題。
Terraform 模組 Modules
模組 (Modules) 是在一個資料夾下的多個組態檔組合,可以被多次的呼叫,以達到重複使用組態資源的目的。
所有的 Terraform 組態都是以模組的型式在運作的,平常執行 terraform 指令所在的工作資料夾,叫做「根模組」(root module)。
模組的好處
如同其他程式語言一樣,模組化帶來很多的好處:
- 更容易整理組態
- 封裝組態
- 不用重複造輪子
- 提供一致的組態設計方式
呼叫模組
要呼叫模組時,可以在你的組態檔裡使用模組區塊 (module block) 來呼叫模組。Terraform 在遇到模組區塊時會自行載入該模組的組態檔。
module "name" {
source = "module_path"
}
模組區塊一定要有的引數 (argument) 是 source
,填入的值是要使用的模組路徑。
模組的來源可以是在本地的檔案,也可以是遠端的資源。
Terraform Registry 網址: https://registry.terraform.io/
HashiCorp 官方維運的服務,提供各種的供應商外掛跟模組,也可以上傳分享自己製作的模組。
今天我們不一點一點的打造基礎架構,而是要練習用現成的模組完成基礎架構。
你可以在 Terraform Registry 上搜尋適合的模組,Terraform 官方也有準備很多好用的模組,例如 AWS VPC 模組: https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/
打開網頁你會看到詳細的模組說明,右側會有一個快速使用範例。
接下來我們要來試著建立一個完整的基礎架構。完整檔案可以參考 Github 上的範例
建立 main.tf 檔案
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "example-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-northeast-1a", "ap-northeast-1d", "ap-northeast-1d"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
tags = {
Terraform = "true"
Environment = "dev"
}
}
module "web_server_sg" {
source = "terraform-aws-modules/security-group/aws//modules/http-80"
name = "web-server"
description = "sg for http ingress"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = ["0.0.0.0/0"]
}
data "aws_ami" "ubuntu-focal" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"]
}
data "template_file" "user_data" {
template = file("user_data.yaml")
}
module "ec2_instances" {
source = "terraform-aws-modules/ec2-instance/aws"
name = "example-ec2-cluster"
instance_count = 2
ami = data.aws_ami.ubuntu-focal.id
instance_type = "t2.micro"
vpc_security_group_ids = [module.vpc.default_security_group_id, module.web_server_sg.this_security_group_id]
subnet_ids = module.vpc.public_subnets
#subnet_id = module.vpc.public_subnets[0]
user_data = data.template_file.user_data.rendered
tags = {
Terraform = "true"
Environment = "dev"
}
}
這次我們使用了三個模組:
module "vpc"
: 使用terraform-aws-modules/vpc/aws
模組,建立 aws VPCmodule "web_server_sg"
: 使用terraform-aws-modules/security-group/aws//modules/http-80
,建立 http 的安全群組module "ec2_instances"
: 使用terraform-aws-modules/ec2-instance/aws
,建立兩台虛擬伺服器
建立 root output.tf 檔案
想要取得模組的輸出資料,格式是 module.<MODULE NAME>.<OUTPUT NAME>
我們試著把 public subnet 跟 public ip 輸出,範例如下:
output.tf
output "vpc_public_subnets" {
description = "IDs of the VPC's public subnets"
value = module.vpc.public_subnets
}
output "ec2_instance_public_ips" {
description = "Public IP addresses of EC2 instances"
value = module.ec2_instances.public_ip
}
執行 init
每當使用到新的模組是就要執行指令 terraform init
,Terraform 會把模組下載到 .terraform/modules
資料夾裡。
terraform init
Initializing modules...
Downloading terraform-aws-modules/ec2-instance/aws 2.15.0 for ec2_instances...
- ec2_instances in .terraform/modules/ec2_instances
Downloading terraform-aws-modules/vpc/aws 2.51.0 for vpc...
- vpc in .terraform/modules/vpc
Downloading terraform-aws-modules/security-group/aws 3.16.0 for web_server_sg...
- web_server_sg in .terraform/modules/web_server_sg/modules/http-80
- web_server_sg.sg in .terraform/modules/web_server_sg
...
Terraform has been successfully initialized!
.terraform/modules
資料夾裡面大概的長相:
.terraform/modules folder
.terraform/modules
├── ec2_instances
├── modules.json
├── vpc
└── web_server_sg
執行 apply
執行指令 terraform apply
的方法是一樣的,確認執行計畫後輸入 yes
。
可以看到這些模組把我們要的 vpc, security-group, instance 都設定好,其他依賴的資源也一起準備好了。
terraform apply
Apply complete! Resources: 34 added, 0 changed, 0 destroyed.
Outputs:
ec2_instance_public_ips = [
"54.238.87.143",
"3.112.40.26",
]
vpc_public_subnets = [
"subnet-0f1fdfe4cf3d3bda0",
"subnet-09410b2bbd87c5876",
"subnet-0f89ceed33412208b",
]
執行 destroy
結束這一輪的練習,輸入指令 terraform destroy
,並輸入 yes
刪除剛剛建立的基礎架構。
嘗試過如果使用現成的模組後,接下來我們要試著製作自己的模組。
一個基本的模組 (module) 大概會有以下幾個檔案:
README.md
說明模組用途的文件main.tf
主要的組態設定檔variables.tf
定義模組的變數,變數會成為module
區塊的引數 (arguments)outputs.tf
定義模組的輸出,輸出會成為可以從模組外取得的資訊,可以用來傳遞資訊給其他組態
這次的自製模組,我們要在 AWS S3 建立託管的靜態網站。
建立模組資料夾
我們要建立一個名稱為 static-bucket
的模組。
在工作資料夾裡建立 modules
資料夾,再放入 static-bucket
資料夾
一個指令完成這件事:
mkdir -p modules/static-bucket
建立 README
建立 README.md
放入模組的說明
# Static S3 Bucket
This module will create aws s3 bucket
建立組態檔 main.tf
- 用
aws_s3_bucket
區塊建立 S3 的儲存貯體 (bucket) 設定成託管網站 - 模組內不需要
provider
區塊
module main.tf
resource "aws_s3_bucket" "s3_bucket" {
bucket = var.bucket_name
acl = "public-read"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::${var.bucket_name}/*"
]
}
]
}
EOF
website {
index_document = "index.html"
error_document = "error.html"
}
tags = var.tags
}
建立組態檔 variables.tf
- 沒的預設值的變數會是模組的必填的變數
- 我們需要從模組外提供
bucket_name
,宣告bucket_name
不設定預設值
module variables.tf
variable "bucket_name" {
description = "the s3 bucket name."
type = string
}
variable "tags" {
type = map(string)
default = {}
}
建立組態檔 outputs.tf
- 輸出一些模組外需要的資料
module outputs.tf
output "arn" {
description = "ARN of the bucket"
value = aws_s3_bucket.s3_bucket.arn
}
output "name" {
description = "Name (id) of the bucket"
value = aws_s3_bucket.s3_bucket.id
}
output "website_endpoint" {
description = "Domain name of the bucket"
value = aws_s3_bucket.s3_bucket.website_endpoint
}
到目前我們完成了自製的模組,檔案清單如下:
tree modules folder
tree modules/
modules/
└── static-s3-bucket
├── README.md
├── main.tf
├── outputs.tf
└── variables.tf
但是現在還沒辦法實際的運作。
要在根模組 (root module) 載入自製的模組,確定它是可以正常使用的。
Terraform 模組
到目前為止我們介紹了一些程式碼書寫的知識,但我們創建的所有資源和資料來源的程式碼都是我們在程式碼檔案中編寫出來的。我們有沒有辦法不透過複製貼上程式碼而直接使用別人寫的 Terraform 程式碼來創建一組資源呢?
Terraform 對此給的答案就是模組( Module )。簡單來講模組就是包含一組 Terraform 程式碼的資料夾,我們之前篇章中寫的程式碼其實也是在模組中。要真正理解模組的功能,我們需要去體驗模組的使用。
Terraform 模組是編寫高品質 Terraform 程式碼,提升程式碼多用性的重要手段,可以說,一個成熟的生產環境應該是由數個可信成熟的模組組裝而成的。我們將在本章介紹關於模組的知識。
建立模組
實際上所有包含 Terraform 程式碼檔案的資料夾都是一個 Terraform 模組。我們如果直接在一個資料夾內執行 terraform apply
或 terraform plan
命令,那麼目前所在的資料夾就被稱為根模組(root module)。我們也可以在執行 Terraform 指令時透過命令列參數指定根模組的路徑。
模組結構
旨在被重複使用的模組與我們編寫的根模組使用的是相同的 Terraform 程式碼和程式碼風格規格。一般來講,在一個模組中,會有:
- 一個
README
文件,用來描述模組的用途。檔案名稱可以是README
或者README.md
,後者應採用 Markdown 語法編寫。可以考慮在README
中用可視化的圖形來描繪創建的基礎設施資源以及它們之間的關係。README
中不需要描述模組的輸入輸出,因為工具會自動收集相關資訊。如果在README
中引用了外部文件或圖片,請確保使用的是帶有特定版本號的絕對 URL 路徑以防止未來指向錯誤的版本 - 一個
LICENSE
描述模組使用的授權協議。如果你想要公開發布一個模組,最好考慮包含一個明確的許可證協議文件,許多組織不會使用沒有明確許可證協議的模組 - 一個 examples 資料夾用來給一個呼叫樣例(可選)
- 一個
variables.tf
文件,包含模組所有的輸入變數。輸入變數應有明確的描述說明用途 - 一個
outputs.tf
文件,包含模組所有的輸出值。輸出值應該有明確的描述說明用途 - 嵌入模組資料夾,出於封裝複雜性或複用程式碼的目的,我們可以在 modules 子目錄下建立一些嵌入模組。所有包含
README
文件的嵌入模組都可以被外部用戶使用;不含README
文件的模組被認為是僅在當前模組內使用的(可選) - 一個
main.tf
,它是模組主要的入口點。對於一個簡單的模組來說,可以把所有資源都定義在裡面;如果是一個比較複雜的模組,我們可以把創建的資源分佈到不同的程式碼檔案中,但引用嵌入模組的程式碼還是要保留在main.tf
裡 - 其他定義了各種基礎設施物件的程式碼檔案(可選)
如果模組含有多個嵌入模組,那麼應避免它們彼此之間的引用,由根模組負責組合它們。
由於 examples/
中的程式碼經常會被拷貝到其他項目中進行修改,所有在 examples/
程式碼中引用本模組時使用的引用路徑應使用外部呼叫者可以使用的路徑,而非相對路徑。
一個最小化模組推薦的結構是這樣的:
tree minimal-module folder
tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
一個更完整一些的模組結構可以是這樣的:
tree complete-module folder
tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│ ├── nestedA/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── nestedB/
│ ├── .../
├── examples/
│ ├── exampleA/
│ │ ├── main.tf
│ ├── exampleB/
│ ├── .../
避免過深的模組結構
我們剛才提到可以在 modules/
子目錄下建立嵌入模組。Terraform 倡導"扁平"的模組結構,只應保持一層嵌入模組,防止在嵌入模組中繼續建立嵌入模組。嵌入模組應設計成易於組合的結構,使得在根模組中可以透過組合各個嵌入模組來創建複雜的基礎設施。
引用模組
在 Terraform 程式碼中引用一個模組,使用的是 module
區塊。
每當在程式碼中新增、刪除或修改一個 module
區塊之後,都要執行 terraform init
或是 terraform get
指令來取得模組程式碼並安裝到本機磁碟上。
模組源
module
區塊定義了一個 source
參數,指定了模組的來源;Terraform 目前支援如下模組來源:
- 本地路徑
- Terraform Registry
- GitHub
- Bitbucket
- 通用Git、Mercurial倉庫
- HTTP位址
- S3 buckets
- GCS buckets
我們後面會一一講解這些模組源的使用。source
使用的是 URL 風格的參數,但某些來源支援在 source
參數中透過額外參數指定模組版本。
出於消除重複程式碼的目的我們可以重構我們的根模組程式碼,將一些擁有重複模式的程式碼重構為可重複呼叫的嵌入模組,透過本地路徑來引用。
許多的模組來源類型都支援從目前系統環境中讀取認證訊息,例如環境變數或系統設定檔。我們在介紹模組來源的時候會介紹到這方面的資訊。
我們建議每個模組把期待被重複使用的基礎設施聲明在各自的根模組位置上,但是直接引用其他模組的嵌入模組也是可行的。
本地路徑
使用本地路徑可以使我們引用同一專案內定義的子模組:
module "consul" {
source = "./consul"
}
一個本機路徑必須以 ./
或 ../
為前綴來標示要使用的本機路徑,以區別於使用 Terraform Registry 路徑。
本機路徑引用模組和其他來源類型有一個區別,本地路徑引用的模組不需要下載相關原始碼,程式碼已經存在於本地相關路徑的磁碟上了。
Terraform Registry
Registry 目前是 Terraform 官方力推的模組倉庫方案,採用了 Terraform 客製化的協議,支援版本化管理和使用模組。
官方提供的公共倉庫保存和索引了大量公共模組,在這裡可以輕鬆搜尋到各種官方和社區提供的高品質模組。
讀者也可以透過 Terraform Cloud 服務維護一個私有模組倉庫,或是透過實作 Terraform 模組註冊協定來實現一個私有倉庫。
公共倉庫的的模組可以用 <NAMESPACE>/<NAME>/<PROVIDER>
形式的來源位址來引用,在公共倉庫上的模組介紹頁面上都包含了確切的來源位址,例如:
module "consul" {
source = "hashicorp/consul/aws"
version = "0.1.0"
}
對於那些託管在其他倉庫的模組,在來源位址頭部新增 <HOSTNAME>/
部分,指定私有倉庫的主機名稱:
module "consul" {
source = "app.terraform.io/example-corp/k8s-cluster/azurerm"
version = "1.1.0"
}
如果你使用的是 SaaS 版本的 Terraform Cloud,那麼託管在上面的私有倉庫的主機名稱是 app.terraform.io
。如果使用的是私有部署的 Terraform 企業版,那麼託管在上面的私有倉庫的主機名稱就是 Terraform 企業版服務的主機名稱。
模組倉庫支援版本化。你可以在 module
區塊中指定模組的版本約束。
如果要引用私有倉庫的模組,你需要先透過設定命令列工具設定檔來設定存取憑證。
GitHub
Terraform 發現 source
當參數的值如果是以 github.com
為前綴時,會將其自動辨識為一個 GitHub 來源:
module "consul" {
source = "github.com/hashicorp/example"
}
上面的例子會自動使用 HTTPS 協定來複製倉庫。如果要使用 SSH 協議,那麼請使用如下的位址:
module "consul" {
source = "git@github.com:hashicorp/example.git"
}
GitHub 來源的處理與後面要介紹的通用 Git 倉庫是一樣的,所以他們取得 git 憑證和透過 ref
參數引用特定版本的方式都是一樣的。如果要存取私有倉庫,你需要額外設定 git 憑證。
Bitbucket
Terraform 發現 source
當參數的值如果是以 bitbucket.org
為前綴時,會將其自動辨識為一個 Bitbucket 來源:
module "consul" {
source = "bitbucket.org/hashicorp/terraform-consul-aws"
}
這種捷徑方法只針對公共倉庫有效,因為 Terraform 必須存取 ButBucket API 來了解倉庫使用的是 Git 還是 Mercurial 協定。
Terraform 根據倉庫的類型來決定將它作為一個 Git 倉庫還是 Mercurial 倉庫來處理。後面的章節會介紹如何為存取倉庫設定存取憑證以及指定要使用的版本號。
通用 Git 倉庫
可以透過在位址開頭加上特殊的 git::
前綴來指定使用任意的 Git 倉庫。在前綴後面跟隨的是一個合法的 Git URL。
使用 HTTPS 和SSH 協定的範例:
module "vpc" {
source = "git::https://example.com/vpc.git"
}
module "storage" {
source = "git::ssh://username@example.com/storage.git"
}
Terraform 使用 git clone
指令安裝模組程式碼,所以 Terraform 會使用本機 Git 系統配置,包括存取憑證。若要存取私有 Git 倉庫,必須先配置對應的憑證。
如果使用了 SSH 協議,那麼會自動使用系統設定的 SSH 證書。通常情況下我們透過這種方法存取私有倉庫,因為這樣可以不需要互動式提示就可以存取私有倉庫。
如果使用 HTTP/HTTPS 協議,或其他需要使用者名稱、密碼作為憑證,你需要設定 Git 憑證儲存來選擇一個合適的憑證來源。
預設情況下,Terraform 會克隆預設分支。可以透過 ref
參數來指定版本:
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
ref 參數會被用作 git checkout 指令的參數,可以是分支名或是 tag 名。
使用 SSH 協定時,我們更推薦 ssh://
的位址。你也可以選擇 scp 風格的語法,故意忽略 ssh://
的部分,只留 git::
,例如:
module "storage" {
source = "git::username@example.com:storage.git"
}
通用 Mercurial 倉庫
可以透過在位 址開頭加上特殊的 hg::
前綴來指定使用任意的 Mercurial 倉庫。在前綴後面跟隨的是一個合法的 Mercurial URL:
module "vpc" {
source = "hg::http://example.com/vpc.hg"
}
Terraform 會透過執行 hg clone
指令從 Mercurial 倉庫安裝模組程式碼,所以 Terraform 會使用本地 Mercurial 系統配置,包括存取憑證。若要存取私有 Mercurial 倉庫,必須先配置對應的憑證。
如果使用了 SSH 協議,那麼會自動使用系統設定的 SSH 證書。通常情況下我們透過這種方法存取私有倉庫,因為這樣可以不需要互動式提示就可以存取私有倉庫。
類似 Git 來源,我們可以透過 ref
參數指定非預設的分支或標籤來選擇特定版本:
module "vpc" {
source = "hg::http://example.com/vpc.hg?ref=v1.2.0"
}
HTTP 位址
當我們使用 HTTP 或 HTTPS 位址時,Terraform 會向指定 URL 發送 GET 請求,期待返回另一個來源位址。這種間接的方法使得 HTTP 可以成為一個更複雜的模組來源位址的指標。